﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Magiq.Behaviors;
using Magiq.Query;
using Magiq.Support;
using Magiq.Update;

namespace Magiq
{
    public static class MagiqExtensions
    {
        public static IUpdate<T> Set<T, TReturn>(this IEnumerable<T> self, Expression<Func<T, TReturn>> property,
                                                 TReturn value)
            where T : class
        {
            return self.Magiq().Set(property, value);
        }

        public static IUpdate<T> Set<T, TReturn>(this IEnumerable<T> self, Expression<Func<T, TReturn>> property,
                                                 Expression<Func<T, TReturn>> value) where T : class
        {
            return self.Magiq().Set(property, value);
        }

        public static int Delete<T>(this IEnumerable<T> self) where T : class
        {
            return self.Magiq().Delete();
        }

        public static int Delete<T>(this IEnumerable<T> self, Expression<Func<T, bool>> where) where T : class
        {
            return self.Magiq().Delete(where);
        }

        public static IMagiq<T> Using<T>(this IEnumerable<T> self, IMagiqBehavior behavior) where T : class
        {
            return self.Magiq().Using(behavior);
        }

        public static IMagiq<T> Magiq<T>(this IEnumerable<T> self) where T : class
        {
            if (!(self is IQueryable<T>))
                throw new InvalidOperationException(
                    "Magiq methods should be called to instances of IQueryable<T>. Use another override. (IEnumerable<T> support is just for expression parsing)");

            return new MagiqOperations<T>((IQueryable<T>)self, global::Magiq.Magiq.BehaviorChain);
        }

        private static readonly MethodInfo[] UpdateMethods = typeof(MagiqExtensions).GetMethods()
               .Where(x => x.Name == "Set")
               .ToArray();

        public static IQueryable<TChild> Query<T, TChild>(this T self, Expression<Func<T, IEnumerable<TChild>>> collection)
            where T : class
        {
            var strategy = global::Magiq.Magiq.ForQuery(new QueryInfo<T, TChild>(self, collection));
            return strategy.Execute();
        }

        public static int Delete<T, TChild>(this T self, Expression<Func<T, IEnumerable<TChild>>> collection)
            where T : class
            where TChild : class
        {
            var split = ExpressionSplit<T, TChild>.From(collection);

            var query = self.Query(split.PropertyExpression);
            if (split.WhereExpression != null)
                query = query.Where(split.WhereExpression);

            return query.Delete();
        }

        public static int Update<T, TChild>(this T self, Expression<Func<T, IUpdate<TChild>>> collection)
            where T : class
            where TChild : class
        {
            var operations = new List<Operation>();

            MethodCallExpression call;
            var rest = collection.Body;
            var contunue = true;

            while (contunue)
            {
                call = rest as MethodCallExpression;
                if (call != null && call.Method.GetGenericMethodDefinition().In(UpdateMethods))
                {
                    operations.Add(new Operation(call.Arguments.Skip(1).ToArray()));
                    rest = call.Arguments[0];
                }
                else
                {
                    contunue = false;
                }
            }

            var split = ExpressionSplit<T, TChild>.From(Expression.Lambda(rest, collection.Parameters.ToArray()));

            var query = self.Query(split.PropertyExpression);

            if (split.WhereExpression != null)
                query = query.Where(split.WhereExpression);

            var magiqQuery = (IUpdateProvider<TChild>)query.Magiq();
            var update = magiqQuery.GetUpdate();

            foreach (var operation in operations)
            {
                LambdaExpression valueExpression;
                var property = ((UnaryExpression)operation[0]).Operand;
                if (operation[1] is ConstantExpression)
                    valueExpression = Expression.Lambda(operation[1], Expression.Parameter(typeof(T), "x"));
                else if (operation[1] is UnaryExpression)
                    valueExpression = (LambdaExpression)((UnaryExpression)operation[1]).Operand;
                else
                    throw new InvalidOperationException("Expression contains unknown methods for update.");

                update.SetByLambdas((LambdaExpression)property, valueExpression);
            }

            if (update == null)
                throw new InvalidOperationException("You should specify values to set!");

            return update.Execute();
        }

        public static int Insert<T,TNew>(this IQueryable<T> self, IQueryable<TNew> source) 
            where T : class
            where TNew : T
        {
            return self.Magiq().Insert(source);
        }

         public static int Insert<T,TNew>(this IQueryable<T> self, Expression<Func<IEnumerable<TNew>>> source) 
            where T : class
            where TNew : T
        {
            return self.Magiq().Insert(source);
        }

         public static int InsertInto<T, TDestination, TNew>(this T self, Expression<Func<T, IEnumerable<TDestination>>> into, IQueryable<TNew> values)
             where TDestination : class
             where T : class
             where TNew : TDestination
         {
             var destination = self.Query(into);
             return destination.Magiq().Insert(values);
         }

        private class Operation
        {
            public Operation(Expression[] arguments)
            {
                this.arguments = arguments;
            }

            private readonly Expression[] arguments;

            public Expression this[int i]
            {
                get { return arguments[i]; }
            }
        }
    }
}